//第一步：搭建结构，读取配置参数，这里接受的是webpack.config.js中的参数
//第二步：用配置参数对象初始化 `Compiler` 对象
//第三步：挂载配置文件中的插件
//第四步：执行`Compiler`对象的`run`方法开始执行编译
//第五步：初始化Compilation，执行build，根据配置文件中的`entry`配置项找到所有的入口
//第六步：从入口文件出发，调用配置的 `loader` 规则，对各模块进行编译
//第七步：找出此模块所依赖的模块，再对依赖模块进行编译
//第八步：等所有模块都编译完成后，根据模块之间的依赖关系，组装代码块 `chunk`（一般来说，每个入口文件会对应一个代码块`chunk`，每个代码块`chunk`里面会放着本入口模块和它依赖的模块）
//第九步：把各个代码块 `chunk` 转换成一个一个文件加入到输出列表
//第十步：确定好输出内容之后，根据配置的输出路径和文件名，将文件内容写入到文件系统（这里就是硬盘）
const { SyncHook } = require("tapable")
const path = require('path');
const fs = require('fs');
const parser = require('@babel/parser'); //解析器
const types = require("@babel/types"); //用来生成或者判断节点的AST语法树的节点
const traverse = require('@babel/traverse').default; //用来遍历AST语法树的节点
const generator = require('@babel/generator').default; //根据AST用来生成代码
const prettier = require("prettier");

function getUnixPath(dir) {
  return dir.replace(/\\/g, '/')
}


//获取文件路径
function tryExtensions(modulePath, extensions) {
  if (fs.existsSync(modulePath)) {
    return modulePath;
  }
  for (let i = 0; i < extensions.length; i++) {
    let filePath = modulePath + extensions[i];
    if (fs.existsSync(filePath)) {
      return filePath;
    }
  }
  throw new Error(`${modulePath} is not exist`);
}

function getSource(chunk) {
  return `
    (()=>{
      var modules = {
        ${chunk.modules.map(module => {
    return `"${module.id}":(module) => {
            ${module.sourceCode}
          }`
  })}
      };
      
      var cache = {};
      function require(moduleId){
        var cachedModule = cache[moduleId];
        if(cachedModule !== undefined) {
          return cachedModule.exports;
        }
        var module = (cache[moduleId] = {
          exports:{},
        });
        modules[moduleId](module,module.exports,require);

        return module.exports;
      };

      var exports = {};
      ${chunk.entryModule.sourceCode}

    })()
  `
}

const baseDir = getUnixPath(__dirname) //命令运行的根目录


class Compilation {
  constructor(webpackConfig) {
    this.webpackConfig = webpackConfig;
    this.modules = []; //存放着所有模块的路径
    this.chunks = [];  //本次编译产出的所有代码块，入口模块和依赖的模块打包在一起为代码块
    this.assets = {}; //本次编译产出的资源文件
    this.fileDependencies = []; //本次打包涉及到的文件，这里主要是为了实现watch模式下监听文件的变化，文件发生变化后会重新编译
  }
  build(callback) {
    let { entry } = this.webpackConfig;
    //entry配置有如下两种，字符串和对象形式，分别对应单入口和多入口，为了后续处理方便，统一处理成第二种
    // {entry:'xxx'} || {entry:{nameKey:'xxxx'}}
    if (typeof entry === 'string') {
      entry = { main: entry }
    }
    // 遍历处理entry,将入口打包成模块
    for (let entryName in entry) {
      //把入口文件的绝对路径添加到依赖数组中，记录此次编译依赖的模块
      let entryPath = entry[entryName];
      //获取入口文件的绝对路径
      let entryPositionPath = path.posix.join(baseDir, entryPath)
      //将入口文件添加到依赖文件中，watch模式会遍历监听该依赖数组来监听文件变化
      this.fileDependencies.push(entryPositionPath);
      //调用模块打包方法，将入口打包成模块
      let entryModule = this.buildModule(entryName, entryPositionPath);
      //将打包后的模块添加到当前Compilation对象的模块列表中
      this.modules.push(entryModule);

      //将模块的id和模块的源码进行拼接，生成chunk
      const chunk = {
        chunkName: entryName,
        entryModule,
        modules: this.modules.filter(m => m.names.includes(entryName)), //过滤出当前chunk的模块
      }
      this.chunks.push(chunk); //将chunk添加到当前Compilation对象的chunks数组中
    }
    this.chunks.forEach(chunk => {
      let fileName = this.webpackConfig.output.filename.replace('[name]', chunk.chunkName); //这里的[name]会被替换成代码块的名称
      this.assets[fileName] = getSource(chunk); //将chunk转化成代码，添加到输出列表中
    })

    callback(null, {
      chunks: this.chunks,
      modules: this.modules,
      assets: this.assets
    }, this.fileDependencies)
  }
  //模块打包方法
  buildModule(entryName, entryPositionPath) {
    //初始化模块
    let module = {
      id: '',  //模块id
      names: entryName,//模块所属的chunk名
      dependencies: [], //该模块依赖的其他模块
      sourceCode: '', //模块的源码
    }
    //获取模块id，模块id为模块的相对路径
    module.id = './' + path.posix.relative(baseDir, entryPositionPath)

    //获取模块的源码
    module.sourceCode = fs.readFileSync(entryPositionPath, 'utf8')

    //获取模块的loader配置
    const loaders = [];
    let { rules = [] } = this.webpackConfig.module;
    for (const rule of rules) {
      let { test, use } = rule;
      if (entryPositionPath.match(test)) {
        loaders.push(...use)
      }
    }

    // 遍历loader，从后往前执行loader，返回处理sourceCode
    module.sourceCode = loaders.reduceRight((code, loader) => {
      let loaderFunc = require(loader.loader);
      return loaderFunc(code)
    }, module.sourceCode)

    //将源码转化成AST，然后找出该模块依赖的其他模块
    let ast = parser.parse(module.sourceCode, { sourceType: 'module' })
    //遍历AST语法树，找出依赖模块
    traverse(ast, {
      CallExpression: (nodePath) => {
        const { node } = nodePath;
        if (node.callee.name === 'require') {
          const depModuleStr = node.arguments[0].value; //获取依赖的模块
          let depModulePath = path.posix.join(path.posix.dirname(entryPositionPath), depModuleStr) //获取依赖模块的绝对路径
          let extensions = this.webpackConfig?.resolve?.extensions || ['.js']; //获取配置中的extensions
          depModulePath = tryExtensions(depModulePath, extensions) //尝试添加后缀名,找到一个真实在硬盘上存在的文件
          this.fileDependencies.push(depModulePath); //将依赖模块的绝对路径 push 到 `this.fileDependencies` 中,为了watch使用
          const depModuleId = './' + path.posix.relative(baseDir, depModulePath) //获取依赖模块的id
          node.arguments = [types.stringLiteral(depModuleId)]
          module.dependencies.push({ depModuleId, depModulePath }) //将依赖模块的id添加到当前模块的依赖数组中
        }
      }
    })
    let { code } = generator(ast) //将AST语法树转化成代码
    module.sourceCode = code //将新生成的代码绑定到sourceCode上

    module.dependencies.forEach(({ moduleId, depModulePath }) => {
      //考虑到多入口打包 ：一个模块被多个其他模块引用，不需要重复打包
      let exitModule = this.modules.find((item) => item.id === moduleId)

      if (exitModule) {
        exitModule.names.push(entryName) //如果模块已经存在了，就把当前模块的名字push进去
      } else {
        let depModule = this.buildModule(entryName, depModulePath) //如果模块不存在，就递归调用buildModule方法，继续打包
        this.modules.push(depModule)
      }
    })
    return module //返回打包后的模块
  }
}

class Compiler {
  constructor(webpackConfig) {
    this.webpackConfig = webpackConfig; //读取webpack配置
    this.hooks = {   //初始化各种钩子（这里简易版，只挂载两个）
      run: new SyncHook(),  //开始编译钩子
      done: new SyncHook()   //编译完成钩子
    }
  }
  compiler(callback) {
    //每一次执行编译，都会初始化一个compilation对象（在watch模式下，文件更新会重新编译，这个时候就会产生新的compilation对象）
    //每个compilation都包含了本词编译的模块、依赖、chunks、assets等各种信息，但是没有像compiler一样的钩子，所以compilation只能操作当前的这次编译
    let compilation = new Compilation(this.webpackConfig);
    compilation.build(callback)
  }
  run(callback) {
    //开始编译，触发开始钩子
    this.hooks.run.call()
    //编译结束
    const onCompilerFinished = (error, stats, fileDependencies) => {
      //第十不，将assets输出到指定目录下
      for (let filename in stats.assets) {
        const asset = stats.assets[filename]
        let filePath = path.posix.join(this.webpackConfig.output.path, filename); //拼接输出路径
        fs.mkdirSync(this.webpackConfig.output.path, { recursive: true }) //创建输出目录
        //输出内容格式化
        prettier.format(asset, { parser: "babel" }).then((result) => {
          //将编译好的文件内容写入到硬盘上
          fs.writeFileSync(filePath, result, {
          });
        })
      }
      callback(error, {
        toJson() {
          return stats
        }
      })
      fileDependencies.forEach(fileDep => {
        fs.watchFile(fileDep, () => {
          //监听到文件变化，重新编译
          this.compiler(onCompilerFinished)
        })
      })
      //编译完成，触发完成钩子
      this.hooks.done.call()
    }

    this.compiler(onCompilerFinished)
  }
}

//第一步：搭建结构，读取配置参数，这里接受的是webpack.config.js中的参数
module.exports = function webpack(webpackConfig) {
  //第二步：用配置参数对象初始化 `Compiler` 对象
  const compiler = new Compiler(webpackConfig)
  //第三步：挂载配置文件中的插件
  const { plugins } = webpackConfig
  // 遍历插件数组，执行每个插件的apply方法
  for (const plugin of plugins) {
    // apply方法是插件的必要方法，参数是compiler对象
    plugin.apply(compiler)
  }
  return compiler
}